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
2 changes: 2 additions & 0 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
odoo-addon-webhook_outgoing @ git+https://github.com/OCA/webhook@refs/pull/15/head#subdirectory=webhook_outgoing
odoo-addon-queue_job @ git+https://github.com/OCA/queue@17.0#subdirectory=queue_job
86 changes: 86 additions & 0 deletions webhook_incoming/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
================
Incoming Webhook
================

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

.. |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-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fwebhook-lightgray.png?logo=github
:target: https://github.com/OCA/webhook/tree/17.0/webhook_incoming
:alt: OCA/webhook
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/webhook-17-0/webhook-17-0-webhook_incoming
: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/webhook&target_branch=17.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module allow creating an automation that send webhook/requests to
another systems via HTTP.

To create a new automation to send webhook requests, go to Settings >
Automated Actions:

- When add an automation, choose Custom Webhook as action to perform.
- Config Endpoint, Headers and Body Template accordingly.

This webhook action use Jinja and rendering engine, you can draft body
template using Jinja syntax.

**Table of contents**

.. contents::
:local:

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/webhook/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/OCA/webhook/issues/new?body=module:%20webhook_incoming%0Aversion:%2017.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
-------

* Hoang Tran

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

- Hoang Tran <thhoang.tr@gmail.com>

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/webhook <https://github.com/OCA/webhook/tree/17.0/webhook_incoming>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions webhook_incoming/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
16 changes: 16 additions & 0 deletions webhook_incoming/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

{
"name": "Incoming Webhook",
"summary": "Receive incoming webhook requests as trigger to execute tasks.",
"version": "17.0.0.0.1",
"author": "Hoang Tran,Odoo Community Association (OCA)",
"license": "LGPL-3",
"website": "https://github.com/OCA/webhook",
"depends": ["base_automation", "webhook_outgoing", "queue_job"],
"data": [
"views/base_automation_views.xml",
],
"auto_install": True,
}
2 changes: 2 additions & 0 deletions webhook_incoming/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import base_automation
from . import ir_actions_server
224 changes: 224 additions & 0 deletions webhook_incoming/models/base_automation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
import base64
import json
import logging
import traceback
from contextlib import closing

from pytz import timezone

from odoo import (
SUPERUSER_ID,
Command,
_,
api,
exceptions,
fields,
models,
registry,
tools,
)
from odoo.tools import ustr
from odoo.tools.float_utils import float_compare
from odoo.tools.safe_eval import safe_eval

_logger = logging.getLogger(__name__)


class InheritedBaseAutomation(models.Model):
_inherit = "base.automation"

allow_creation = fields.Boolean(
string="Allow creation?",
help="Allow executing webhook to maybe create record if a record is not "
"found using record getter",
)
create_record_code = fields.Text(
"Record Creation Code",
default="""# Available variables:
# - env: Odoo Environment on which the action is triggered
# - model: Odoo Model of the record on which the action is triggered;
# is a void recordset
# - record: record on which the action is triggered; may be void
# - records: recordset of all records on which the action is triggered
# in multi-mode; may be void
# - payload: input payload from webhook request
# - time, datetime, dateutil, timezone: useful Python libraries
# - float_compare: Odoo function to compare floats based on specific precisions
# - log: log(message, level='info'): logging function to record debug information
# in ir.logging table
# - UserError: Warning Exception to use with raise
# - Command: x2Many commands namespace
# You must return the created record by assign it to `record` variable:
# - record = res.partner(1,)
""",
help="Create record if Record Getter couldn't find a matching one.",
)
delay_execution = fields.Boolean(
help="Queue actions to perform to delay execution."
)

def _get_eval_context(self, payload=None):
"""
Override to add payload to context
"""
eval_context = super()._get_eval_context()
eval_context["model"] = self.env[self.model_name]
eval_context["payload"] = payload if payload is not None else {}
return eval_context

Check warning on line 69 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L66-L69

Added lines #L66 - L69 were not covered by tests

def _execute_webhook(self, payload):
"""Execute the webhook for the given payload.
The payload is a dictionnary that can be used by the `record_getter` to
identify the record on which the automation should be run.
"""
self.ensure_one()

Check warning on line 76 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L76

Added line #L76 was not covered by tests

# info logging is done by the ir.http logger
msg = "Webhook #%s triggered with payload %s"
msg_args = (self.id, payload)
_logger.debug(msg, *msg_args)

Check warning on line 81 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L79-L81

Added lines #L79 - L81 were not covered by tests

record = self.env[self.model_name]
eval_context = self._get_eval_context(payload=payload)

Check warning on line 84 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L83-L84

Added lines #L83 - L84 were not covered by tests

if self.record_getter:
try:
record = safe_eval(self.record_getter, eval_context)
except Exception as e: # noqa: BLE001
msg = (

Check warning on line 90 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L87-L90

Added lines #L87 - L90 were not covered by tests
"Webhook #%s couldn't be triggered because record_getter failed:"
"\n%s"
)
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))
raise e

Check warning on line 97 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L94-L97

Added lines #L94 - L97 were not covered by tests

if not record.exists() and self.allow_creation:
try:
create_eval_context = self._get_create_eval_context(payload=payload)
safe_eval(

Check warning on line 102 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L100-L102

Added lines #L100 - L102 were not covered by tests
self.create_record_code,
create_eval_context,
mode="exec",
nocopy=True,
) # nocopy allows to return 'action'
record = create_eval_context.get("record", self.model_id.browse())
except Exception as e: # noqa: BLE001
msg = "Webhook #%s failed with error:\n%s"
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))

Check warning on line 113 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L108-L113

Added lines #L108 - L113 were not covered by tests

elif not record.exists():
msg = (

Check warning on line 116 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L116

Added line #L116 was not covered by tests
"Webhook #%s could not be triggered because "
"no record to run it on was found."
)
msg_args = (self.id,)
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, msg)
raise exceptions.ValidationError(

Check warning on line 123 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L120-L123

Added lines #L120 - L123 were not covered by tests
_("No record to run the automation on was found.")
)

try:
res = self._process(record)
self._webhook_logging(payload, None)
return res
except Exception as e: # noqa: BLE001
msg = "Webhook #%s failed with error:\n%s"
msg_args = (self.id, traceback.format_exc())
_logger.warning(msg, *msg_args)
self._webhook_logging(payload, self._add_postmortem(e))
raise e

Check warning on line 136 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L127-L136

Added lines #L127 - L136 were not covered by tests

def _get_create_eval_context(self, payload=None):
def log(message, level="info"):

Check warning on line 139 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L139

Added line #L139 was not covered by tests
with self.pool.cursor() as cr:
cr.execute(

Check warning on line 141 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L141

Added line #L141 was not covered by tests
"""
INSERT INTO ir_logging(
create_date, create_uid, type, dbname, name,
level, message, path, line, func
)
VALUES (
NOW() at time zone 'UTC', %s,
%s, %s, %s, %s,
%s, %s, %s, %s,
)
""",
(
self.env.uid,
"server",
self._cr.dbname,
__name__,
level,
message,
"action",
self.id,
self.name,
),
)

eval_context = dict(self.env.context)
model_name = self.model_id.sudo().model
model = self.env[model_name]
eval_context.update(

Check warning on line 169 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L166-L169

Added lines #L166 - L169 were not covered by tests
{
"uid": self._uid,
"user": self.env.user,
"time": tools.safe_eval.time,
"datetime": tools.safe_eval.datetime,
"dateutil": tools.safe_eval.dateutil,
"timezone": timezone,
"float_compare": float_compare,
"b64encode": base64.b64encode,
"b64decode": base64.b64decode,
"Command": Command,
"env": self.env,
"model": model,
"log": log,
"payload": payload,
}
)
return eval_context

Check warning on line 187 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L187

Added line #L187 was not covered by tests

def _webhook_logging(self, body, response):
if self.log_webhook_calls:
with closing(registry(self.env.cr.dbname).cursor()) as cr:
env = api.Environment(cr, SUPERUSER_ID, {})

Check warning on line 192 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L192

Added line #L192 was not covered by tests

def create_log(env, response):
vals = {

Check warning on line 195 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L194-L195

Added lines #L194 - L195 were not covered by tests
"webhook_type": "incoming",
"webhook": f"{self.name} ({self})",
"endpoint": f"/web/hook/{self.webhook_uuid}",
"headers": {},
"request": json.dumps(ustr(body), indent=4),
"body": json.dumps(ustr(body), indent=4),
"response": ustr(response),
"status": getattr(response, "status_code", None),
}
env["webhook.logging"].create(vals)
env.cr.commit()

Check warning on line 206 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L205-L206

Added lines #L205 - L206 were not covered by tests

create_log(env, response)

Check warning on line 208 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L208

Added line #L208 was not covered by tests

def _process(self, records, domain_post=None):
"""
Override to allow delay execution
"""
to_delay = self.filtered(lambda a: a.delay_execution)
execute_now = self - to_delay

Check warning on line 215 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L215

Added line #L215 was not covered by tests

super(

Check warning on line 217 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L217

Added line #L217 was not covered by tests
InheritedBaseAutomation,
to_delay.with_context(delay_execution=True),
)._process(records, domain_post=domain_post)

return super(InheritedBaseAutomation, execute_now)._process(

Check warning on line 222 in webhook_incoming/models/base_automation.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/base_automation.py#L222

Added line #L222 was not covered by tests
records, domain_post=domain_post
)
21 changes: 21 additions & 0 deletions webhook_incoming/models/ir_actions_server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2024 Hoang Tran <thhoang.tr@gmail.com>.
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from odoo import api, models


class IrActionsServer(models.Model):
_inherit = "ir.actions.server"

def run(self):
if self.env.context.get("delay_execution"):
return self.with_delay().run()
return super().run()

Check warning on line 12 in webhook_incoming/models/ir_actions_server.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/ir_actions_server.py#L11-L12

Added lines #L11 - L12 were not covered by tests

@api.model
def _job_prepare_context_before_enqueue_keys(self):
return (

Check warning on line 16 in webhook_incoming/models/ir_actions_server.py

View check run for this annotation

Codecov / codecov/patch

webhook_incoming/models/ir_actions_server.py#L16

Added line #L16 was not covered by tests
"active_model",
"active_ids",
"active_id",
"domain_post",
)
3 changes: 3 additions & 0 deletions webhook_incoming/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions webhook_incoming/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Hoang Tran \<<thhoang.tr@gmail.com>\>
11 changes: 11 additions & 0 deletions webhook_incoming/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
This module allow creating an automation that send webhook/requests to
another systems via HTTP.

To create a new automation to send webhook requests, go to Settings \>
Automated Actions:

- When add an automation, choose Custom Webhook as action to perform.
- Config Endpoint, Headers and Body Template accordingly.

This webhook action use Jinja and rendering engine, you can draft body
template using Jinja syntax.
Loading