Metadata-Version: 2.1
Name: odoo-addon-usability_webhooks
Version: 18.0.4.0.0
Requires-Python: >=3.10
Requires-Dist: odoo==18.0.*
Summary: REST API for Webhook
Home-page: https://github.com/ecosoft-odoo/ecosoft-addons
License: AGPL-3
Author: Ecosoft, Odoo Community Association (OCA)
Author-email: support@odoo-community.org
Classifier: Programming Language :: Python
Classifier: Framework :: Odoo
Classifier: Framework :: Odoo :: 18.0
Classifier: License :: OSI Approved :: GNU Affero General Public License v3
Description-Content-Type: text/x-rst

.. image:: https://odoo-community.org/readme-banner-image
   :target: https://odoo-community.org/get-involved?utm_source=readme
   :alt: Odoo Community Association

====================
REST API for Webhook
====================

.. 
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! This file is generated by oca-gen-addon-readme !!
   !! changes will be overwritten.                   !!
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
   !! source digest: sha256:555b1674af58543eca00d3fd9349e5d6c1334518528915844a1fa04653952770
   !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
    :target: https://odoo-community.org/page/development-status
    :alt: Beta
.. |badge2| image:: https://img.shields.io/badge/license-AGPL--3-blue.png
    :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
    :alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-ecosoft--odoo%2Fecosoft--addons-lightgray.png?logo=github
    :target: https://github.com/ecosoft-odoo/ecosoft-addons/tree/18.0/usability_webhooks
    :alt: ecosoft-odoo/ecosoft-addons

|badge1| |badge2| |badge3|

This module provides a standard webhook framework for Odoo with full
request/response logging and config-driven outbound push notifications.

**Inbound (External → Odoo)**

- 5 REST API routes: ``create_data``, ``update_data``,
  ``create_update_data``, ``search_data``, ``call_function``
- Session-based and API Key authentication
- Friendly relational field format: ``many2one``, ``many2many``,
  ``one2many`` resolved by name or id
- ``auto_create`` support for missing related records
- Automatic API log creation per request, with configurable per-route
  toggle
- Request and response **preview** (first N characters, configurable)
  stored on the log record
- Request and response **size** (character count) displayed on each log
- Full payload stored as JSON attachment (accessible via **Full Log**
  button) when preview limit exceeded
- Autovacuum cron to purge old logs, with optional chunk-based deletion

**Outbound (Odoo → External)**

- ``webhook.outbound.rule`` - config-driven rules: which model + domain
  → which endpoint
- ``webhook.outbound.mixin`` - add to any model with a single
  ``_inherit`` line; no per-model code required
- **Trigger domain**: full Odoo domain expression evaluated after
  ``write()``; webhook fires only when a record transitions into
  matching the domain
- **Endpoint source**: static URL per rule, or per-record
  ``callback_url`` passed by the external system at create time
- **Payload fields**: JSON list supporting ``field{sub1,sub2}``
  expansion for relational fields - same syntax as ``search_data``
- Outbound calls logged in API Logs (``log_type = send``) with
  success/failed state

**Table of contents**

.. contents::
   :local:

Configuration
=============

System Parameters
-----------------

Go to *Settings > Technical > Parameters > System Parameters* to adjust
the following keys:

+---------------------------------------+----------+-----------------------------+
| Key                                   | Default  | Description                 |
+=======================================+==========+=============================+
| ``webhook.preview_limit``             | ``2000`` | Maximum characters stored   |
|                                       |          | in the preview fields.      |
|                                       |          | Payloads longer than this   |
|                                       |          | are also saved as a full    |
|                                       |          | JSON attachment.            |
+---------------------------------------+----------+-----------------------------+
| ``webhook.create_data_log``           | ``True`` | Enable logging for          |
|                                       |          | ``/api/create_data``        |
+---------------------------------------+----------+-----------------------------+
| ``webhook.update_data_log``           | ``True`` | Enable logging for          |
|                                       |          | ``/api/update_data``        |
+---------------------------------------+----------+-----------------------------+
| ``webhook.create_update_data_log``    | ``True`` | Enable logging for          |
|                                       |          | ``/api/create_update_data`` |
+---------------------------------------+----------+-----------------------------+
| ``webhook.search_data_log``           | ``True`` | Enable logging for          |
|                                       |          | ``/api/search_data``        |
+---------------------------------------+----------+-----------------------------+
| ``webhook.call_function_log``         | ``True`` | Enable logging for          |
|                                       |          | ``/api/call_function``      |
+---------------------------------------+----------+-----------------------------+
| ``webhook.rollback_state_failed``     | ``1``    | Roll back the transaction   |
|                                       |          | when the API response is    |
|                                       |          | not successful              |
+---------------------------------------+----------+-----------------------------+
| ``webhook.rollback_except``           | ``1``    | Roll back the transaction   |
|                                       |          | when an unhandled exception |
|                                       |          | occurs                      |
+---------------------------------------+----------+-----------------------------+
| ``webhook.ignore_checkcompany_model`` | ``[]``   | JSON list of model names    |
|                                       |          | excluded from               |
|                                       |          | company-scoped record       |
|                                       |          | lookup                      |
+---------------------------------------+----------+-----------------------------+

Outbound Webhook Rules
----------------------

Go to *Settings > Technical > API Configuration > Outbound Webhook
Rules* to configure outbound push rules.

+--------------------------+-------------------------------------------+
| Field                    | Description                               |
+==========================+===========================================+
| **Model**                | The Odoo model to watch (e.g.             |
|                          | ``sale.order``)                           |
+--------------------------+-------------------------------------------+
| **Trigger Domain**       | Odoo domain evaluated after ``write()``.  |
|                          | Webhook fires when a record transitions   |
|                          | into matching the domain. Uses the domain |
|                          | widget - select a model first to get      |
|                          | field suggestions.                        |
+--------------------------+-------------------------------------------+
| **Endpoint Source**      | ``Static URL`` - always POST to the       |
|                          | configured URL. ``Record Callback URL`` - |
|                          | use the ``callback_url`` stored from the  |
|                          | inbound request.                          |
+--------------------------+-------------------------------------------+
| **Endpoint URL**         | Required when Endpoint Source is          |
|                          | ``Static URL``.                           |
+--------------------------+-------------------------------------------+
| **Payload Fields**       | JSON list of field names to include.      |
|                          | Supports ``field{sub1,sub2}`` for         |
|                          | relational expansion. Leave empty to send |
|                          | ``{"id": <record_id>}`` only.             |
+--------------------------+-------------------------------------------+
| **Authorization Header** | Optional ``Authorization`` header value   |
|                          | sent with every outbound request, e.g.    |
|                          | ``Bearer <token>``.                       |
+--------------------------+-------------------------------------------+

Usage
=====

Inbound (External → Odoo)
-------------------------

API Logs
~~~~~~~~

Every API call is logged under *Settings > Technical > API Configuration
> API Logs*. Each log record shows:

- **Request Preview** / **Response Preview** - first N characters of the
  payload
- **Request Size** / **Response Size** - total character count
- **Full Log** button - opens the full JSON attachment when payload
  exceeds the preview limit
- **Callback URL** - URL stored from the inbound request for later
  outbound push

Authentication
~~~~~~~~~~~~~~

Authenticate via ``/web/session/authenticate`` before calling any route:

.. code:: json

   {
     "jsonrpc": "2.0",
     "method": "call",
     "params": {
       "db": "<db_name>",
       "login": "<username>",
       "password": "<password>"
     }
   }

**Alternative - API Key:** send ``Authorization: Bearer <api_key>`` on
every request. No session call needed.

Relational Field Format
~~~~~~~~~~~~~~~~~~~~~~~

+---------------+-------------------------------------------------+-----------------------------------------------+
| Field type    | Format                                          | Example                                       |
+===============+=================================================+===============================================+
| ``many2one``  | ``{"<lookup_field>": "<value>"}``               | ``{"name": "Customer A"}`` or ``{"id": 5}``   |
+---------------+-------------------------------------------------+-----------------------------------------------+
| ``many2many`` | ``{"mode": "add"|"replace", "records": [...]}`` | ``{"records": [{"name": "Tag1"}]}``           |
|               | (``mode`` defaults to ``"replace"``)            |                                               |
+---------------+-------------------------------------------------+-----------------------------------------------+
| ``one2many``  | ``[{<field>: <value>, ...}, ...]``              | ``[{"product_id": {"name": "A"}, "qty": 1}]`` |
+---------------+-------------------------------------------------+-----------------------------------------------+

Multiple ``many2many`` items sharing the same lookup field are batched
into a single DB query.

API Routes
~~~~~~~~~~

1. ``/api/create_data`` - create a new record
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Pass optional ``callback_url`` to enable outbound push when the record's
state changes later.

.. code:: json

   {
     "params": {
       "model": "<model name>",
       "vals": {
         "callback_url": "https://your-system/webhook",
         "payload": {
           "<field1>": "<value1>",
           "<many2one_field_id>": {"name": "<value>"},
           "<many2many_field_ids>": {"mode": "replace", "records": [{"name": "<val1>"}]},
           "<one2many_field_ids>": [
             {"<field>": "<value>", "<nested_m2o_id>": {"name": "<value>"}}
           ]
         },
         "auto_create": {
           "<many2one_field_id>": {"name": "<value>"}
         },
         "result_field": ["<field1>"]
       }
     }
   }

2. ``/api/create_update_data`` - update if found, create if not
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: json

   {
     "params": {
       "model": "<model name>",
       "vals": {
         "search_key": {"<key_field>": "<value>"},
         "payload": {
           "<field1>": "<value1>",
           "<many2one_field_id>": {"name": "<value>"}
         },
         "result_field": ["<field1>"]
       }
     }
   }

3. ``/api/update_data`` - update an existing record
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. code:: json

   {
     "params": {
       "model": "<model name>",
       "vals": {
         "search_key": {"<key_field>": "<value>"},
         "payload": {
           "<field1>": "<value1>",
           "<many2one_field_id>": {"id": 5},
           "<many2many_field_ids>": {"mode": "add", "records": [{"name": "<val1>"}]}
         },
         "result_field": ["<field1>"]
       }
     }
   }

4. ``/api/search_data`` - query records
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Use ``field{subfield1,subfield2}`` to expand relational fields inline.

.. code:: json

   {
     "params": {
       "model": "<model name>",
       "vals": {
         "payload": {
           "search_field": [
             "<field1>",
             "<m2o_field>{<subfield1>,<subfield2>}",
             "<o2m_field>{<subfield1>}"
           ],
           "search_domain": "[('<field>', '<operator>', '<value>')]",
           "limit": 10,
           "order": "<field1> asc, <field2> desc"
         }
       }
     }
   }

5. ``/api/call_function`` - call a method on a record
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

- ``method`` *(str)*: method name
- ``parameter`` *(dict, optional)*: keyword arguments
- ``context`` *(dict, optional)*: merged into ``env.context`` before the
  call

.. code:: json

   {
     "params": {
       "model": "account.move",
       "vals": {
         "search_key": {"id": 26},
         "payload": {
           "method": "action_post",
           "context": {"lang": "th_TH"}
         }
       }
     }
   }

Attaching Files
~~~~~~~~~~~~~~~

Add ``attachment_ids`` at any payload level:

.. code:: json

   "attachment_ids": [{"name": "<filename>", "datas": "<base64>"}]

--------------

Outbound (Odoo → External)
--------------------------

When Odoo performs an action (confirm, validate, etc.), the outbound
webhook automatically POSTs updated record data back to the external
system - no per-model code required.

Step 1 - Add mixin to the target model
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In any private addon, add one ``_inherit`` line:

.. code:: python

   from odoo import models

   class SaleOrder(models.Model):
       _name = "sale.order"
       _inherit = ["sale.order", "webhook.outbound.mixin"]

All outbound behaviour is driven by rules configured in the UI.

Step 2 - Configure an Outbound Webhook Rule
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Go to *Settings > Technical > API Configuration > Outbound Webhook
Rules*. See ``CONFIGURE.md`` for the full field reference.

**Trigger domain examples:**

.. code:: python

   # Simple
   [("state", "=", "sale")]

   # Multiple conditions
   [("state", "=", "done"), ("amount_total", ">", 100)]

   # Multiple accepted values
   [("state", "in", ["done", "validated"])]

The webhook fires only when a field in the domain is being written
**and** the record matches the full domain after the write. This
prevents re-triggering when unrelated fields are edited on an
already-matching record.

Payload Fields - relational expansion
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Use ``field{sub1,sub2}`` syntax (same as ``search_data``) to expand
relational fields:

.. code:: json

   ["name", "state", "currency_id{id,name,code}", "order_line{product_id,qty_done,price_unit}"]

Result posted to the external system:

.. code:: json

   {
     "name": "SO001",
     "state": "sale",
     "currency_id": [{"id": 3, "name": "Thai Baht", "code": "THB"}],
     "order_line": [
       {"product_id": 5, "qty_done": 2.0, "price_unit": 500.0}
     ]
   }

``many2one`` fields expand to a list with one item (consistent with
``search_data`` behaviour).

Per-record Callback URL
~~~~~~~~~~~~~~~~~~~~~~~

Pass ``callback_url`` in the inbound ``create_data`` request. Odoo
stores it linked to the created record. When the outbound rule fires
with *Endpoint Source = Record Callback URL*, the system looks up that
URL and POSTs to it.

.. code:: json

   {
     "params": {
       "model": "sale.order",
       "vals": {
         "callback_url": "https://ext-system/webhook/so-status",
         "payload": {
           "partner_id": {"name": "ABC Co."},
           "order_line": [{"product_id": {"name": "Product A"}, "product_uom_qty": 1}]
         }
       }
     }
   }

When the SO is confirmed → Odoo automatically POSTs to
``https://ext-system/webhook/so-status``.

Outbound Logs
~~~~~~~~~~~~~

All outbound calls appear in *API Logs* with **Log Type = Send**. Failed
calls are marked ``state = failed`` with the error in the response
preview.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/ecosoft-odoo/ecosoft-addons/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 <https://github.com/ecosoft-odoo/ecosoft-addons/issues/new?body=module:%20usability_webhooks%0Aversion:%2018.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.

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

Credits
=======

Authors
-------

* Ecosoft

Contributors
------------

- Kitti Upariphutthiphong kittiu@ecosoft.co.th
- Saran Lim. saranl@ecosoft.co.th

Maintainers
-----------

This module is part of the `ecosoft-odoo/ecosoft-addons <https://github.com/ecosoft-odoo/ecosoft-addons/tree/18.0/usability_webhooks>`_ project on GitHub.

You are welcome to contribute.
